#!/usr/bin/env python
#-*- coding:utf-8 -*-

"""
    Este modulo define la clase Ecuacion, que permite interpretar una ecuacion
        de una cadena de caracteres y separala en sus componentes, para ser
        almacenados en notacion polaca inversa.    
"""
import re

class Ecuacion(object):
    """
        Representa una ecuacion, separada por operadores.
        Utiliza notacion polaca inversa para almacenar los componentes de la operacion.        
    """
    def __init__(self, operadores):
        self._operadores = operadores
        self._componentes = list()

        self._abrir = [ '(', '[', '{' ] # Parentesis de apertura
        self._cerrar = [ ')', ']', '}' ] # Parentesis de cierre.
        
        # expresion regular para separar usando los operadores y los parentesis.
        aux = '('
        aux += ')|('.join( [ re.escape(x) for x in self._abrir ] )
        aux += ')|('
        aux += ')|('.join( [ re.escape(x) for x in self._cerrar] )
        aux += ')|('
        aux += ')|('.join( [ re.escape(x) for x in self._operadores] )
        aux += ')|\b'

        self._patron = re.compile(aux)

    def is_operador(self, operador):
        """
            Permite saber si un string es uno de los operadores definidos.            
        """
        return operador in self._operadores

    def aridad_de(self, operador):
        """
            Obtiene la aridad de del operador si existe.
            Si el operador no existe devuelve -1.            
        """
        aridad = None
        try: 
            aridad = self._operadores[operador]
        except KeyError:
            aridad = -1
        return aridad

    def __str__(self):
        return self.to_infija()

    def get_componentes(self):
        """
            Obtiene los componentes de la ecuacion.            
        """
        return self._componentes


    def from_infija(self, string):
        """
            Lee una ecuacion en notacion infija y lo convierte a notacion
                polaca inversa, almacenando sus componentes por separado.
            Ejemplo:
                "A+B" se transformara en ["A", "B", "+"], si se ha definido como
                operador a "+".
        """

        componentes = list() # Componentes de la expresion en NPR.
        variables = [list()] # Pila de variables (no son operadores).
        operadores = list() # Pila de operadores.

        grado = 0 # El grado de parentesis encontrandos.
        aridad = None

        partes = [ x for x in self._patron.split(string) if x is not None ]
        partes = [ x.strip() for x in partes if x.strip() != '' ]
        
        for parte in partes:
            
            if self.is_operador(parte):
                operadores.append(parte)
            elif parte in self._abrir:
                grado += 1
                while len(variables) < grado+1:
                    variables.append(list())
            else:
                if not parte in self._cerrar:
                    variables[grado].append(parte)

                while len(operadores)>0:
                    operador = operadores[-1]
                    aridad = self.aridad_de(operador)

                    if len(variables[grado]) >= aridad:
                        operador = operadores.pop()

                        # Elimino los elementos None de la lista.
                        aux = variables[grado][:aridad]
                        aux = [ x for x in aux if x is not None ]
                        componentes.extend(aux)

                        variables[grado] = variables[grado][aridad:]
                        componentes.append(operador)
                        variables[grado-1].append(None)

                        if len(variables[grado]) == 0:
                            grado -= 1
                            variables.pop()
                    else:
                        break

        # Si la ecuacion era correcta el grado debe ser cero.
        if None in variables[0]:
            variables[0].remove(None)

        # Todas las pilas tienen que estar vacias.
        cero = len(operadores) + sum( [ len(x) for x in variables ] ) + grado

        if cero == 0:
            self._componentes = componentes
        
        return (cero == 0)

    def to_infija(self):
        """
            Obtiene un string representando a la ecuacion en formato infijo.
            Ejemplo: "A+B".
        """
        pila = list()
        for parte in self.get_componentes():
            if not self.is_operador(parte): # No es parentesis por construccion.
                pila.append(parte)
            else:
                # debo operar.
                operador = parte
                aridad = self.aridad_de(operador)

                if aridad <= 0:
                    break
                if aridad == 1:
                    aux = operador+'('+pila.pop()+')'
                    pila.append(aux)
                    
                elif aridad == 2:
                    der = pila.pop()
                    izq = pila.pop()
                    aux = '('+ izq + operador + der +')'
                    pila.append(aux)
                else:
                    parametros = list()
                    while len(parametros) < aridad:
                        parametros.append(pila.pop())
                    aux = operador+'('+ ', '.join(parametros)+')'
                    pila.append(aux)


        if len(pila) == 1:
            resultado = pila.pop()
            if resultado[0] in self._abrir and resultado[-1] in self._cerrar:
                resultado = resultado[1:-1]
            return resultado
        else:
            return None


def _test():
    """
       Ejecuta las pruebas integradas al modulo ecuacion.
    """

    calculadora = Ecuacion({'+':2, '-':2, '*':2, '/':2, '&':2, '|':2,
                            'cos':1, 'f':3 })

    pruebas = [ '1+2', '(1+2)+4', '(1+2)', '(A|B)&C', 'A|[B&C]', 'cos(x)+4',
                'A|B&C', '((A|B)&(B|C)&C)', '1/( 2*(3-4) )' ]

    for caso in pruebas:
        print 'Caso: "'+caso+'"'
        if calculadora.from_infija(caso):
            print ' RESULTADO: '+str(calculadora.get_componentes())
            print '       str: '+str(calculadora)
        else:
            print ' ERROR'


if __name__ == '__main__':
    _test()
